package com.googlecode.totallylazy.json;

import com.googlecode.totallylazy.Callable1;
import com.googlecode.totallylazy.Function1;
import com.googlecode.totallylazy.Maps;
import com.googlecode.totallylazy.Pair;
import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;
import com.googlecode.totallylazy.Triple;
import com.googlecode.totallylazy.parser.Parser;
import com.googlecode.totallylazy.parser.Parsers;
import com.googlecode.totallylazy.parser.Reference;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

import static com.googlecode.totallylazy.Booleans.valueOf;
import static com.googlecode.totallylazy.Characters.hexDigits;
import static com.googlecode.totallylazy.Characters.in;
import static com.googlecode.totallylazy.Predicates.not;
import static com.googlecode.totallylazy.parser.Parsers.isChar;
import static com.googlecode.totallylazy.parser.Parsers.string;
import static com.googlecode.totallylazy.parser.Parsers.ws;
import static com.googlecode.totallylazy.parser.Parsers.wsChar;
import static java.lang.Integer.parseInt;

public class Grammar {
    public static final Parser<String> NULL = string("null").map(new Function1<String, String>() {
        @Override
        public String call(String s) throws Exception {
            return null;
        }
    });
    public static final Parser<Boolean> BOOLEAN = string("true").or(string("false")).map(valueOf);
    public static final Predicate<Character> UNICODE_CHARACTER = not(in("\"\\"));

    public static final Parser<String> ESCAPED_CHARACTER = string(isChar('\\'), isChar(in("\"\\/bfnrt"))).
            or(string(isChar('\\'), isChar('u'), isChar(hexDigits), isChar(hexDigits), isChar(hexDigits), isChar(hexDigits))).map(unescape());

    private static Function1<Sequence<String>, String> join = new Function1<Sequence<String>, String>() {
        @Override
        public String call(Sequence<String> strings) throws Exception {
            return strings.toString("");
        }
    };

    public static final Parser<String> STRING = string(isChar(UNICODE_CHARACTER)).
            or(ESCAPED_CHARACTER).many().map(join).between(isChar('"'), isChar('"'));


    private static Function1<String, String> unescape() {
        return new Function1<String, String>() {
            public String call(String escaped) throws Exception {
                switch (escaped.charAt(1)) {
                    case '"': return "\"";
                    case '\\': return "\\";
                    case '/': return "/";
                    case 'b': return "\b";
                    case 'n': return "\n";
                    case 'r': return "\r";
                    case 't': return "\t";
                    case 'f': return "\f";
                    case 'u': return Character.toString((char) parseInt(escaped.substring(2), 16));
                    default: throw new UnsupportedOperationException();
                }
            }
        };
    }

    public static final Parser<Number> NUMBER = Parsers.decimal.map(new Callable1<String, Number>() {
        public Number call(String value) {
            return new BigDecimal(value);
        }
    });

    public static final Reference<Object> VALUE = Parsers.newReference();

    public static final Parser<Pair<String, Object>> PAIR = Parsers.tripleOf(STRING, wsChar(':'), VALUE).map(new Callable1<Triple<String, Character, Object>, Pair<String, Object>>() {
        @Override
        public Pair<String, Object> call(Triple<String, Character, Object> triple) throws Exception {
            return Pair.pair(triple.first(), triple.third());
        }
    });

    public static final Parser<Character> SEPARATOR = wsChar(',');

    public static final Parser<List<Object>> ARRAY = Parsers.between(wsChar('['), VALUE.separatedBy(SEPARATOR), wsChar(']')).map(new Function1<Sequence<Object>, List<Object>>() {
        @Override
        public List<Object> call(Sequence<Object> objects) throws Exception {
            return objects.toList();
        }
    });

    public static final Parser<java.util.Map<String, Object>> OBJECT = Parsers.between(wsChar('{'), PAIR.sepBy(SEPARATOR), wsChar('}')).map(new Function1<Iterable<Pair<String, Object>>, Map<String, Object>>() {
        @Override
        public Map<String, Object> call(Iterable<Pair<String, Object>> pairs) throws Exception {
            return Maps.map(pairs);
        }
    });

    static {
        VALUE.set(ws(Parsers.<Object>or(OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL)));
    }
}
